﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;
using System.IO;
using System.Threading;
using System.Collections.ObjectModel;
using System.Runtime.CompilerServices;

namespace pHMb.Spectrogram
{
    public partial class SpectrogramPane : PictureBox
    {
        /// <summary>
        /// List of the windows currently in the graph
        /// </summary>
        public List<double[]> Windows { get; private set; }

        [Description("Sets the size of each window in samples")]
        [DefaultValue(4096)]
        public int WindowSamples { get; set; }

        public bool NoUpdate { get; set; }

        private object _imageLocker = new object();

        Color _axesColor = Color.Black;

        public Color AxesColor { get { return _axesColor; } set { _axesColor = value; } }

        public PictureBox OverlayBox { get; private set; }

        public int ScrollHorizOffset { get; set; }
        public int ScrollVPWidth { get; set; }

        bool _scaleGraph = true;
        public bool ScaleGraph
        {
            get { return _scaleGraph; }
            set { _scaleGraph = value; }
        }

        public double WindowSize
        {
            get
            {
                return (1.00 / 44100) * WindowSamples;
            }
        }

        public bool isGreyScale
        {
            get;
            set;
        }

        public class XSelection : INotifyPropertyChanged
        {
            public event PropertyChangedEventHandler PropertyChanged;

            protected void SetProperty<T>(ref T field, T value, [CallerMemberName] string name = "")
            {
                if (!EqualityComparer<T>.Default.Equals(field, value))
                {
                    field = value;
                    var handler = PropertyChanged;
                    if (handler != null)
                    {
                        handler(this, new PropertyChangedEventArgs(name));
                    }
                }
            }

            XSelection _noiseSelection;
            public XSelection NoiseSelection
            {
                get { return _noiseSelection; }
                set { SetProperty(ref _noiseSelection, value); }
            }


            public virtual int StartX { get; set; }
            public virtual int EndX { get; set; }

            private string _name;
            public string Name
            {
                get { return _name; }
                set { SetProperty(ref _name, value); }
            }


            public Color StartLineColor { get; set; }
            public Color EndLineColor { get; set; }

            public EventHandler EndHandler { get; set;}

            public bool IsHighlighted { get; set; }
            public bool IsVisible { get; set; }

            public SpectrogramPane ParentPane { get; private set; }

            public int StartWindowId
            {
                get
                {
                    return this.ParentPane.GetWindowId(this.StartX);
                }
            }

            public int EndWindowId
            {
                get
                {
                    return this.ParentPane.GetWindowId(this.EndX);
                }
            }

            protected double[] _averageFFT = null;
            public double[] AverageFFT
            {
                get
                {
                    if (_averageFFT == null)
                    {
                        List<double> sumWindow = new List<double>();

                        List<double[]> fft = GetWindows();

                        foreach (double[] window in fft)
                        {
                            int j = 0;

                            foreach (double freq in window)
                            {
                                if (sumWindow.Count <= j)
                                {
                                    sumWindow.Add(freq);
                                }
                                else
                                {
                                    sumWindow[j] += freq;
                                }

                                j++;
                            }
                        }

                        _averageFFT = new double[sumWindow.Count];

                        for (int i = 0; i < sumWindow.Count; i++)
                        {
                            _averageFFT[i] = sumWindow[i] / fft.Count;
                        }
                    }

                    return _averageFFT;
                }
            }

            protected double[] _snr = null;
            public virtual double[] SNR
            {
                get
                {
                    if (_snr == null)
                    {
                        _snr = new double[AverageFFT.Length];
                        for (int i = 0; i < AverageFFT.Length; i++)
                        {
                            _snr[i] = AverageFFT[i] - NoiseSelection.AverageFFT[i];
                        }
                    }

                    return _snr;
                }
            }

            protected double[] _normSnr = null;
            public virtual double[] NormSNR
            {
                get
                {
                    if (_normSnr == null)
                    {
                        _normSnr = Normalise(SNR);
                    }

                    return _normSnr;
                }
            }

            public XSelection(SpectrogramPane parentPane)
            {
                this.StartX = -1;
                this.EndX = -1;

                ParentPane = parentPane;

                IsVisible = true;
            }

            public virtual List<double[]> GetWindows()
            {
                List<double[]> windows = new List<double[]>();

                for (int i = StartWindowId; i <= EndWindowId; i++)
                {
                    windows.Add(ParentPane.Windows[i]);
                }

                return windows;
            }

            public override string ToString()
            {
                return string.Format("{0}: windows {1} to {2}", this.Name, this.StartWindowId, this.EndWindowId);
            }

            protected double CalcAvgAmplitude(double[] avgFft, int startF, int endF, bool absIn = false, bool absOut = false)
            {
                double avgAmp = 0;
                int x = 0;
                for (x = (int)(startF / (1.00 / ParentPane.WindowSize)); x < (int)(endF / (1.00 / ParentPane.WindowSize)); x++)
                //for (x = 0; x < avgFft.Length; x++)
                {
                    if (absIn)
                    {
                        avgAmp += avgFft[x];
                    }
                    else
                    {
                        avgAmp += Math.Pow(10, avgFft[x] / 20.00);
                    }
                }

                avgAmp /= x;

                if (absOut)
                {
                    return avgAmp;
                }
                else
                {
                    return 20 * Math.Log10(avgAmp);
                }
            }

            protected double[] Normalise(double[] snr)
            {
                double avgDb100_2000 = CalcAvgAmplitude(snr, 100, 2000, false, false);
                //double avgDb100_2000_Avg = CalcAvgAmplitude(_avgLung, 100, 2000, false, false);

                double normFactor = avgDb100_2000 - 0;

                double[] normSnr = new double[snr.Length];
                for (int i = 0; i < snr.Length; i++)
                {
                    normSnr[i] = snr[i] - normFactor;
                }

                return normSnr;
            }
        }

        public class XSelectionGroup : XSelection
        {
            public List<XSelection> ChildSelections { get; set; }

            public XSelectionGroup(SpectrogramPane parentPane)
                : base(parentPane)
            {
                IsVisible = false;
                ChildSelections = new List<XSelection>();
            }

            public override double[] NormSNR
            {
                get
                {
                    if (_normSnr == null)
                    {
                        List<double> sumWindow = new List<double>();
                        foreach (XSelection childSelection in ChildSelections)
                        {
                            for (int i = 0; i < childSelection.NormSNR.Length; i++)
                            {
                                double snr = childSelection.NormSNR[i];

                                if (sumWindow.Count <= i)
                                {
                                    sumWindow.Add(snr);
                                }
                                else
                                {
                                    sumWindow[i] += snr;
                                }
                            }
                        }

                        _normSnr = new double[sumWindow.Count];
                        for (int i = 0; i < sumWindow.Count; i++)
                        {
                            _normSnr[i] = sumWindow[i] / ChildSelections.Count;
                        }
                    }

                    return _normSnr;
                }
            }

            public override double[] SNR
            {
                get
                {
                    if (_snr == null)
                    {
                        List<double> sumWindow = new List<double>();
                        foreach (XSelection childSelection in ChildSelections)
                        {
                            for (int i = 0; i < AverageFFT.Length; i++)
                            {
                                double snr = childSelection.AverageFFT[i] - childSelection.NoiseSelection.AverageFFT[i];

                                if (sumWindow.Count <= i)
                                {
                                    sumWindow.Add(snr);
                                }
                                else
                                {
                                    sumWindow[i] += snr;
                                }
                            }
                        }

                        _snr = new double[sumWindow.Count];
                        for (int i = 0; i < sumWindow.Count; i++)
                        {
                            _snr[i] = sumWindow[i] / ChildSelections.Count;
                        }
                    }

                    return _snr;
                }
            }

            public override List<double[]> GetWindows()
            {
                List<double[]> windows = new List<double[]>();

                foreach (XSelection selection in ChildSelections)
                {
                    windows.AddRange(selection.GetWindows());
                }

                return windows;
            }

            public override int StartX
            {
                get
                {
                    return ChildSelections.Min(x => x.StartX);
                }
                set
                {
                    //throw new InvalidOperationException("Start X can not be set on a group");
                }
            }

            public override int EndX
            {
                get
                {
                    return ChildSelections.Max(x => x.EndX);
                }
                set
                {
                    //throw new InvalidOperationException("End X can not be set on a group");
                }
            }

            public override string ToString()
            {
                return string.Format("{0}: ({1} win)", this.Name, this.ChildSelections.Count);
            }
        }

        public ObservableCollection<XSelection> SelectedXPoints { get; set; }

        public int Divisor { get; set; }

        public int XAxisHeight { get { return _xAxisHeight; } }
        public int YAxisWidth { get { return _yAxisWidth; } }

        private int _maxHeight;
        int _xAxisHeight = 50;
        int _yAxisWidth = 100;
        int _freqRange = 400;
        int _currentX = 0;
        int _updateTime = 100;

        int _colorBarY = 0;
        int _colorBarX = 0;
        int _colorBarHeight = 0;
        int _colorBarWidth = 5;
        int _colorBarLabelSpacing = 20;

        XSelection _currentXSelection = null;

        static Dictionary<int, int[]> _hueCache;

        Thread _updateThread;

        public SpectrogramPane()
        {
            ScrollHorizOffset = 0;

            // Overlay contains selection ranges etc.
            OverlayBox = new PictureBox();
            OverlayBox.Parent = this;
            OverlayBox.Dock = DockStyle.Fill;
            OverlayBox.BackColor = Color.Transparent;

            OverlayBox.MouseClick += OverlayBox_MouseClick;
            OverlayBox.MouseMove += OverlayBox_MouseMove;
            OverlayBox.MouseLeave += OverlayBox_MouseLeave;
            

            SelectedXPoints = new ObservableCollection<XSelection>();
            SelectedXPoints.CollectionChanged += SelectedXPoints_CollectionChanged;

            Windows = new List<double[]>();
            Divisor = 130;
            InitializeComponent();
            ScrollVPWidth = int.MaxValue;
        }

        void SelectedXPoints_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            UpdateOverlay();
        }

        /// <summary>
        /// Adds a window to the spectrogram
        /// </summary>
        /// <param name="amplitudes"></param>
        public void AddWindow(double[] amplitudes)
        {
            Windows.Add(amplitudes);
            UpdateGraph();
        }

        /// <summary>
        /// Adds an array of windows to the spectrogram
        /// </summary>
        /// <param name="windows"></param>
        public void AddWindows(double[][] windows)
        {
            Windows.AddRange(windows);
            UpdateGraph();
        }

        public void Clear()
        {
            Windows.Clear();
            SelectedXPoints.Clear();
            _currentX = 0;
            this.Image = null;

            if (ScaleGraph)
            {
                this.Width = 400;
            }
            //UpdateGraph();
        }

        /// <summary>
        /// Allows the user to select a point on the graph
        /// </summary>
        /// <param name="selectionName"></param>
        /// <param name="lineColor"></param>
        public void StartSelection(string selectionName, Color startLineColor, Color endLineColor, EventHandler selectionEndCallback = null)
        {
            _currentXSelection = new XSelection(this);
            _currentXSelection.Name = selectionName;
            _currentXSelection.StartLineColor = startLineColor;
            _currentXSelection.EndLineColor = endLineColor;
            _currentXSelection.EndHandler = selectionEndCallback;
        }

        public void StopSelection()
        {
            _currentXSelection = null;
        }

        public void ExportTextFile(string path)
        {
            using (StreamWriter exportWriter = new StreamWriter(path))
            {
                foreach (double[] magnitudes in Windows)
                {
                    foreach (double magnitude in magnitudes)
                    {
                        exportWriter.Write("{0}, ", magnitude);
                    }
                    exportWriter.WriteLine();
                }
            }
        }

        public void UpdateOverlay(int currentHoverPosX = 0, int currentHoverPosY = 0)
        {
            if (!NoUpdate)
            {
                //Bitmap canvas = new Bitmap(OverlayBox.Width, OverlayBox.Height);

                int overlayWidth = (ScrollVPWidth > OverlayBox.Width) ? OverlayBox.Width : ScrollVPWidth;
                int overlayOffset = ScrollHorizOffset;

                //OverlayBox.Width = overlayWidth;
                OverlayBox.Padding = new Padding(overlayOffset, 0, 0, 0);

                Bitmap canvas = new Bitmap(overlayWidth, OverlayBox.Height);

                Graphics offScreenDC = Graphics.FromImage(canvas);

                int width = canvas.Width - _yAxisWidth - this.Padding.Left - this.Padding.Right;
                int height = canvas.Height - _xAxisHeight - this.Padding.Top - this.Padding.Bottom;

                foreach (XSelection xSelection in this.SelectedXPoints)
                {
                    if (xSelection.IsVisible && xSelection.StartX < (overlayOffset + overlayWidth) && xSelection.EndX > overlayOffset)
                    {
                        int startPoint = xSelection.StartX > overlayOffset ? xSelection.StartX - overlayOffset : 0;
                        int endPoint = xSelection.EndX < (overlayOffset + overlayWidth) ? xSelection.EndX - overlayOffset : overlayWidth;

                        offScreenDC.DrawLine(new Pen(xSelection.StartLineColor), new Point(startPoint, height + 10), new Point(startPoint, 0));
                        offScreenDC.DrawLine(new Pen(xSelection.EndLineColor), new Point(endPoint, height + 10), new Point(endPoint, 0));

                        // Draw selection rect
                        offScreenDC.FillRectangle(new SolidBrush(Color.FromArgb(60, 255, 255, 255)),
                            new Rectangle(startPoint, 0, (int)Math.Abs(startPoint - endPoint), height + 10));

                        RectangleF labelRect = new RectangleF(startPoint, 0, endPoint - startPoint, 20);

                        if (xSelection.IsHighlighted)
                        {
                            offScreenDC.FillRectangle(new SolidBrush(Color.FromArgb(80, 255, 0, 0)), labelRect);
                        }

                        StringFormat selectLabelFormat = new StringFormat();
                        selectLabelFormat.Alignment = StringAlignment.Center;
                        offScreenDC.DrawString(xSelection.Name,
                            new Font("Arial", 8, FontStyle.Regular, GraphicsUnit.Pixel),
                            new SolidBrush(AxesColor),
                            labelRect,
                            selectLabelFormat);
                    }
                }

                if (_currentXSelection != null && _currentXSelection.StartX != -1 &&
                    _currentXSelection.StartX < (overlayOffset + overlayWidth))
                {
                    int startPoint = _currentXSelection.StartX > overlayOffset ? _currentXSelection.StartX - overlayOffset : 0;

                    offScreenDC.DrawLine(new Pen(_currentXSelection.StartLineColor), new Point(startPoint, height + 10), new Point(startPoint, 0));
                }

                bool isOverGraph = currentHoverPosX > _yAxisWidth && currentHoverPosY < this.Height - (_xAxisHeight + this.Padding.Bottom);

                if (currentHoverPosX != 0 && isOverGraph)
                {
                    int hozLineStart = 0;

                    if (overlayOffset < _yAxisWidth)
                    {
                        hozLineStart = _yAxisWidth - overlayOffset - 5;
                    }

                    int offsetXHover = currentHoverPosX - overlayOffset;

                    offScreenDC.DrawLine(new Pen(Color.Red), new Point(offsetXHover, height + 20), new Point(offsetXHover, this.Padding.Top));
                    offScreenDC.DrawLine(new Pen(Color.Red), new Point(hozLineStart, currentHoverPosY), new Point(overlayWidth, currentHoverPosY));

                    // Do color bar highlighting
                    int windowId = GetWindowId(currentHoverPosX);
                    int freqId = GetFreqId(currentHoverPosY);

                    double amplitude;
                    if (windowId > 0 && freqId > 0 && windowId < Windows.Count && freqId < Windows[windowId].Length)
                    {
                        amplitude = Windows[windowId][freqId];
                    }
                    else
                    {
                        amplitude = 0;
                    }

                    double colorY = _colorBarHeight * ((Divisor / 360) - (amplitude / Divisor));

                    try
                    {
                        offScreenDC.DrawLine(
                            new Pen(Color.Black),
                            new Point(_colorBarX + 3, (int)colorY + _colorBarY + Padding.Top),
                            new Point(_colorBarX + 3 + _colorBarWidth, (int)colorY + _colorBarY + Padding.Top));
                    }
                    catch { }

                    // Draw current point text

                    Rectangle currentPtRect = new Rectangle(0, _colorBarY + _colorBarHeight + Padding.Top, 60, 40);
                    offScreenDC.FillRectangle(new SolidBrush(Color.LightGray), currentPtRect);
                    offScreenDC.DrawRectangle(new Pen(Color.Black), currentPtRect);

                    offScreenDC.DrawString(String.Format("T: {0:0.0}s\nF: {1:0}Hz\nA: {2:0}dB", WindowSize * windowId, freqId / WindowSize, amplitude),
                        new Font("Arial", 10, FontStyle.Regular, GraphicsUnit.Pixel),
                        new SolidBrush(Color.Black),
                        currentPtRect,
                        new StringFormat() { LineAlignment = StringAlignment.Center });
                }

                OverlayBox.Image = canvas;
                offScreenDC.Dispose();
            }
        }

        public void UpdateGraph()
        {
            if (!NoUpdate)
            {
                if (Windows.Count > 0)
                {
                    lock (_imageLocker)
                    {
                        Bitmap canvas;
                        Graphics offScreenDC;
                        if (this.Image == null)
                        {
                            canvas = new Bitmap(this.Width, this.Height);

                            offScreenDC = Graphics.FromImage(canvas);
                            // Fill with black
                            offScreenDC.FillRegion(new SolidBrush(BackColor), new System.Drawing.Region(new RectangleF(0, 0, this.Width, this.Height)));
                        }
                        else
                        {
                            canvas = (Bitmap)this.Image;
                            offScreenDC = Graphics.FromImage(canvas);
                        }

                        // int _freqRange = Windows.LastOrDefault().Length;

                        // Find width of graph surface
                        int width = canvas.Width - _yAxisWidth - this.Padding.Left - this.Padding.Right;
                        int height = canvas.Height - _xAxisHeight - this.Padding.Top - this.Padding.Bottom;

                        if (height > _maxHeight)
                            _maxHeight = height;



                        if (_currentX == 0)
                        {
                            // Render axis
                            double timePerPx = ((double)WindowSamples / 44100);

                            double currTime = 0;
                            double curMinTime = 0;
                            double majTimeDelim = 2;
                            double minTimeDelim = 0.5;

                            int boxWidth = 50;
                            int textSize = 10;

                            int majDelim = 500;
                            int minDelim = 100;

                            int curFreq = 0;
                            int curMinFreq = 0;

                            double freqPerPx = (double)(_freqRange * (1.00 / WindowSize)) / (double)(height);

                            // Y Axis
                            for (int y = height; y >= 0; y--)
                            {
                                double f = Math.Round((freqPerPx * ((height - y) - 8)), 1);

                                StringFormat strFormat = new StringFormat();
                                strFormat.Alignment = StringAlignment.Far;


                                if (f >= curFreq)
                                {
                                    offScreenDC.DrawString(string.Format("{0} -", curFreq), new Font("Arial", textSize, FontStyle.Regular, GraphicsUnit.Pixel), new SolidBrush(AxesColor), _yAxisWidth, y, strFormat);
                                    curFreq += majDelim;
                                }

                                if (f >= curMinFreq)
                                {
                                    offScreenDC.DrawString("-", new Font("Arial", textSize, FontStyle.Regular, GraphicsUnit.Pixel), new SolidBrush(AxesColor), _yAxisWidth, y, strFormat);
                                    curMinFreq += minDelim;
                                }
                            }

                            // Color key
                            _colorBarHeight = height;
                            PrintColorKey(offScreenDC, _colorBarY, _colorBarX, _colorBarHeight, _colorBarWidth, _colorBarLabelSpacing);


                            StringFormat yAxisLabelFormat = new StringFormat();
                            yAxisLabelFormat.Alignment = StringAlignment.Center;
                            yAxisLabelFormat.FormatFlags = StringFormatFlags.DirectionVertical;
                            //offScreenDC.DrawString("Frequency (Hz)", new Font("Arial", textSize, FontStyle.Regular, GraphicsUnit.Pixel), new SolidBrush(AxesColor), new RectangleF(_yAxisWidth, height + 30, width, 12), yAxisLabelFormat);

                            // X Axis
                            for (int x = 0; x < width + _yAxisWidth; x++)
                            {

                                double t = Math.Round(((timePerPx * ((x - _yAxisWidth + boxWidth / 2)))), 1);

                                StringFormat strFormat = new StringFormat();
                                strFormat.Alignment = StringAlignment.Center;

                                if (t >= currTime)
                                {
                                    offScreenDC.DrawString("|", new Font("Arial", textSize, FontStyle.Bold, GraphicsUnit.Pixel), new SolidBrush(AxesColor), new RectangleF(x, height, boxWidth, height), strFormat);
                                    offScreenDC.DrawString(string.Format("{0}", currTime), new Font("Arial", textSize, FontStyle.Regular, GraphicsUnit.Pixel), new SolidBrush(AxesColor), new RectangleF(x, height + 13, boxWidth, height), strFormat);
                                    currTime += majTimeDelim;
                                }



                                if (t >= curMinTime)
                                {
                                    offScreenDC.DrawString("|", new Font("Arial", textSize, FontStyle.Regular, GraphicsUnit.Pixel), new SolidBrush(AxesColor), new RectangleF(x, height, boxWidth, height), strFormat);
                                    curMinTime += minTimeDelim;
                                }
                            }

                            StringFormat xAxisLabelFormat = new StringFormat();
                            xAxisLabelFormat.Alignment = StringAlignment.Center;
                            offScreenDC.DrawString("Time (s)", new Font("Arial", textSize, FontStyle.Regular, GraphicsUnit.Pixel), new SolidBrush(AxesColor), new RectangleF(_yAxisWidth, height + 30, width, 12), xAxisLabelFormat);
                        }


                        // lock image
                        PixelFormat format = canvas.PixelFormat;
                        BitmapData data = canvas.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, format);
                        int stride = data.Stride;
                        int offset = stride - width * 4 - _yAxisWidth * 4;

                        try
                        {
                            unsafe
                            {
                                byte* pixel = (byte*)data.Scan0.ToPointer();

                                int maxX = 0;
                                // for each column
                                for (int y = 0; y <= height - 1; y++)
                                {
                                    pixel += _yAxisWidth * 4;
                                    // Skip over already set values
                                    pixel += _currentX * 4;

                                    // for each row
                                    int x = 0;
                                    for (x = _currentX; x < width; x++, pixel += 4)
                                    {
                                        if (Windows.Count > width && x == width - 1)
                                        {
                                            maxX = width;
                                        }
                                        else if (x < Windows.Count)
                                        {
                                            //int windowId = Windows.Count - y - 1;
                                            //int freqId = (_freqRange * x) / width;

                                            int windowId = x;
                                            int freqId = _freqRange - ((_freqRange * y) / height) - 1;

                                            double amplitude = Windows[windowId][freqId];
                                            int[] color;
                                            if (this.isGreyScale)
                                            {

                                                int greyColor = (int)((amplitude + 150) * (255 / Divisor));

                                                if (greyColor > 255) greyColor = 255;

                                                color = new int[] { 255 - greyColor, 255 - greyColor, 255 - greyColor, 255 };
                                            }
                                            else
                                            {
                                                // Divisor specifies the range covered by the color
                                                // a full hue value (360) is locked to 0dB always (max amp.)
                                                color = RotateHue((amplitude + Divisor) * (360f / Divisor));
                                            }

                                            for (int i = 0; i < 3; i++)
                                            {
                                                if (color[i] > 255) color[i] = 255;
                                                pixel[i] = (byte)color[i];
                                            }

                                            pixel[3] = (byte)255;
                                        }
                                        else
                                        {
                                            maxX = x;
                                            break;
                                        }
                                    }

                                    if (x != width - 1)
                                    {
                                        pixel += (width - x) * 4;
                                    }

                                    pixel += offset;
                                }


                                _currentX = maxX;
                            }
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine(ex.ToString());
                        }

                        // unlock image
                        canvas.UnlockBits(data);




                        // Clean up
                        this.Image = canvas;
                        offScreenDC.Dispose();


                        if (_currentX >= width - 1)
                        {
                            if (this.Dock == DockStyle.Fill || !ScaleGraph)
                            {
                                Windows.RemoveRange(0, Windows.Count / 5);
                                _currentX = 0;

                                this.Image = null;
                                UpdateGraph();
                            }
                            else
                            {
                                this.Width += 100;
                            }
                        }
                    }
                }
            }
        }

        private void PrintColorKey(
            Graphics offScreenDC,
            int colorBarY,
            int colorBarX,
            int colorBarHeight,
            int colorBarWidth = 5,
            int colorBarLabelSpacing = 20)
        {
            int paddingTop = 10;
            int paddingBottom = 20;
            int paddingLeft = 3;

            Rectangle backRect = new Rectangle(colorBarX, colorBarY, colorBarWidth + 55, colorBarHeight);
            offScreenDC.FillRectangle(new SolidBrush(Color.LightGray), backRect);
            offScreenDC.DrawRectangle(new Pen(Color.Black), backRect);

            // Add internal padding
            colorBarY += paddingTop;
            colorBarX += paddingLeft;
            colorBarHeight -= paddingBottom;

            double pixelsPerColor = colorBarHeight / 360d;

            for (int y = 0; y < colorBarHeight; y++)
            {
                if (pixelsPerColor == 0)
                    break;

                double hue = (double)(colorBarHeight - y) / pixelsPerColor;

                if (hue < 360)
                {
                    int canvasY = colorBarY + y;

                    int[] colorArray = RotateHue(hue);

                    offScreenDC.DrawRectangle(new Pen(Color.FromArgb(colorArray[2], colorArray[1], colorArray[0])), new Rectangle(colorBarX, canvasY - (int)pixelsPerColor / 2, colorBarWidth, (int)pixelsPerColor));

                    if (y % colorBarLabelSpacing == 0)
                    {
                        double amplitude = ((hue) * Divisor) / 360 - Divisor;

                        StringFormat strFormat = new StringFormat();
                        strFormat.LineAlignment = StringAlignment.Center;

                        Rectangle stringRect = new Rectangle(colorBarX + colorBarWidth - 1, canvasY - 10, 50, 20);
                        Font strFont = new Font("Arial", 10, FontStyle.Regular, GraphicsUnit.Pixel);

                        offScreenDC.DrawString(string.Format("  {0:0}dB", amplitude),
                            strFont,
                            new SolidBrush(Color.Black),
                            stringRect, strFormat);

                        offScreenDC.DrawString("-",
                            strFont,
                            new SolidBrush(Color.FromArgb(colorArray[2], colorArray[1], colorArray[0])),
                            stringRect, strFormat);
                    }
                }
            }
        }

        public double GetTimeFromX(int x)
        {
            if (x < this.Width && x > _yAxisWidth && Windows.Count > 0)
            {
                double timePerPx = ((double)WindowSamples / 44100);

                return Math.Round(((timePerPx * ((x - _yAxisWidth)))), 1);
            }
            else
            {
                return -1;
            }
        }

        public double GetFreqFromY(int y)
        {
            if (y < this.Height - _xAxisHeight && Windows.Count > 0)
            {
                int height = this.Height - _xAxisHeight - this.Padding.Top - this.Padding.Bottom;
                double freqPerPx = (double)(_freqRange * (1.00 / WindowSize)) / (double)(height);

                return Math.Round((freqPerPx * ((height - y) - 8)), 1);
            }
            else
            {
                return -1;
            }
        }

        public int GetWindowId(int x)
        {
            return x - _yAxisWidth;
        }

        public int GetFreqId(int y)
        {
            int height = this.Height - _xAxisHeight - this.Padding.Top - this.Padding.Bottom;
            return _freqRange - ((_freqRange * (y - this.Padding.Top)) / height) - 1;
        }

        private void UpdateLoop()
        {
            while (!IsDisposed && this.IsHandleCreated)
            {
                this.Invoke(new Action(UpdateGraph));
                Thread.Sleep(_updateTime);
            }
        }

        /// <summary>
        /// Selects colours depending on value of hue
        /// </summary>
        /// <param name="hue"></param>
        /// <returns></returns>
        private static int[] RotateHue(double hue)
        {
            if (_hueCache == null)
            {
                _hueCache = new Dictionary<int, int[]>();
            }

            int originalHue = (int)hue;

            if (_hueCache.ContainsKey(originalHue))
            {
                return _hueCache[originalHue];
            }
            else
            {

                double f, q;
                int i;

                if (hue >= 360f || hue <= 0)
                    hue = 0;

                hue /= 60f;
                i = (int)hue;
                f = hue - (double)i;
                q = 1 - f;

                int r, g, b;

                switch (i)
                {
                    case 0:
                        r = 255; g = (int)(255 * f) % 256; b = 0;
                        break;
                    case 1:
                        r = (int)(255 * q) % 256; g = 255; b = 0;
                        break;
                    case 2:
                        r = 0; g = 255; b = (int)(255 * f) % 256;
                        break;
                    case 3:
                        r = 0; g = (int)(255 * q) % 256; b = 255;
                        break;
                    case 4:
                        r = (int)(255 * f) % 256; g = 0; b = 255;
                        break;
                    default:
                        r = 255; g = 0; b = (int)(255 * q) % 256;
                        break;
                }

                _hueCache[originalHue] = new int[] { r, g, b };
                return _hueCache[originalHue];
            }
        }

        public static int[] RotateHue2(double hue)
        {
            double value = 255;
            double saturation = 255;

            int hi = Convert.ToInt32(Math.Floor(hue / 60)) % 6;
            double f = hue / 60 - Math.Floor(hue / 60);

            value = value * 255;
            int v = Convert.ToInt32(value);
            int p = Convert.ToInt32(value * (1 - saturation));
            int q = Convert.ToInt32(value * (1 - f * saturation));
            int t = Convert.ToInt32(value * (1 - (1 - f) * saturation));

            if (hi == 0)
                return new int[] {v, t, p};
            else if (hi == 1)
                return new int[] {q, v, p};
            else if (hi == 2)
                return new int[] {p, v, t};
            else if (hi == 3)
                return new int[] {p, q, v};
            else if (hi == 4)
                return new int[] {t, p, v};
            else
                return new int[] {v, p, q};
        }

        public class MouseMoveGraphArgs : EventArgs
        {
            public double Frequency { get; set; }
            public double Time { get; set; }
            public double Amplitude { get; set; }
        }




        void OverlayBox_MouseLeave(object sender, EventArgs e)
        {
            UpdateOverlay();
        }

        void OverlayBox_MouseMove(object sender, MouseEventArgs e)
        {
            UpdateOverlay(e.X, e.Y);
        }

        void OverlayBox_MouseClick(object sender, MouseEventArgs e)
        {
            // Handle selection if one is in process

            if (_currentXSelection != null)
            {
                // Check we are in graph area
                if (e.X >= _yAxisWidth && e.X < Windows.Count + _yAxisWidth && e.Y < this.Height - _xAxisHeight && Windows.Count > 0)
                {
                    // This is a select event yay!
                    if (_currentXSelection.StartX == -1)
                    {
                        _currentXSelection.StartX = e.X;

                        UpdateOverlay();
                    }
                    else
                    {
                        // Check point isn't before / same as start
                        if (e.X < _currentXSelection.StartX)
                        {
                            _currentXSelection.EndX = _currentXSelection.StartX;
                            _currentXSelection.StartX = e.X;
                        }
                        else if (e.X == _currentXSelection.StartX)
                        {
                            _currentXSelection.EndX = e.X;
                        }
                        else
                        {
                            _currentXSelection.EndX = e.X;
                        }

                        // Add to selected points
                        this.SelectedXPoints.Add(_currentXSelection);

                        XSelection oldSelection = _currentXSelection;
                        _currentXSelection = null;

                        if (oldSelection.EndHandler != null)
                        {
                            oldSelection.EndHandler(this, new EventArgs());
                        }

                        UpdateOverlay();
                    }
                }
            }
        }


        public event EventHandler<MouseMoveGraphArgs> MouseMoveGraph;
        protected void OnMouseMoveGraph (MouseMoveGraphArgs e)
        {
            if (MouseMoveGraph != null)
            {
                MouseMoveGraph.Invoke(this, e);
            }
        }

        protected override void OnHandleCreated(EventArgs e)
        {
            _updateThread = new Thread(new ThreadStart(UpdateLoop));
            _updateThread.Start();

            base.OnHandleCreated(e);
        }

        protected override void OnHandleDestroyed(EventArgs e)
        {
            _updateThread.Abort();

            base.OnHandleDestroyed(e);
        }

        protected override void OnSizeChanged(EventArgs e)
        {
            base.OnSizeChanged(e);
            _currentX = 0;
            this.Image = null;
            UpdateGraph();
        }

        protected override void OnMouseMove(MouseEventArgs e)
        {
            if (e.X > _yAxisWidth && e.Y < this.Height - _xAxisHeight && Windows.Count > 0)
            {
                double freqPerPx = (double)(_freqRange * (1.00 / WindowSize)) / (double)(this.Width - _yAxisWidth);
                double timePerPx = ((double)WindowSamples / 44100);

                MouseMoveGraphArgs args = new MouseMoveGraphArgs()
                {
                    Frequency = (freqPerPx * ((e.X - _yAxisWidth))),
                    Time = timePerPx * e.Y
                };

                int windowId = Windows.Count - e.Y - 1;
                int freqId = (_freqRange * (e.X - _yAxisWidth)) / (this.Width - _yAxisWidth);

                if (windowId > 0 && windowId < Windows.Count && freqId < Windows[windowId].Length)
                {
                    args.Amplitude = Windows[windowId][freqId];
                }
                else
                {
                    args.Amplitude = 0;
                }

                // TODO: Handle user selection display



                OnMouseMoveGraph(args);

            }

            base.OnMouseMove(e);
        }

    }
}
